package hypeerweb;

import communications.GlobalObjectId;
import communications.LocalObjectId;
import communications.ObjectDB;
import communications.PeerCommunicator;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;

/**
 * Main HyPeerWeb class
 *
 * @author Dan Hogue
 */
public class HyPeerWeb implements IHyPeerWebObserver, Serializable {

	private static final int MAX_CONCURRENT_READS = 100;

	private static HyPeerWeb singleton;

	private HyPeerWeb parentSegment = null;
	private List<IHyPeerWebObserver> observers = new ArrayList<IHyPeerWebObserver>();

	private ArrayList<Node> nodes;
	private HyPeerWebDatabase db;
	private static final Logger log = Logger.getLogger(HyPeerWebDatabase.class.getName());

	private Semaphore writeLock = new Semaphore(1, true);
	private Semaphore readQueue = new Semaphore(MAX_CONCURRENT_READS, true);

	public static final int HYPEER_WEB_LOCAL_ID = Integer.MIN_VALUE;

	/**
	* Gets the single HyPeerWeb, if one doesn't exist then one is created.
	*
	* @pre <i>None</i>
	* @post singleton' = null ⇒ singleton = new HyPeerWeb() AND result = singleton
	* @return singleton hypeerweb instance
	*/
	public static HyPeerWeb getSingleton() {
	if(singleton == null) {
	    singleton = new HyPeerWeb();
	}
	return singleton;
	}

	/**
	* Private Constructor
	*/
	private HyPeerWeb() {
	nodes = new ArrayList<Node>();
	db = HyPeerWebDatabase.getSingleton();
	}

	protected HyPeerWeb(boolean isProxy)
	{
		assert isProxy;
		nodes = null;
		db = null;
	}

	private void clearNodes() {
		nodes.clear();
		db.clear();
	}

	private void openReadLock()
	{
		// Sending command message
		try {
			readQueue.acquire();

		} catch (InterruptedException ex) {
			Logger.getLogger(HyPeerWeb.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	private void openWriteLock()
	{
		// Sending command message
		try {
			// Want to ensure only one write (not entirely sure this is a 
			// necessary semaphore)
			writeLock.acquire();
			// Consume all readQueue permits. Ensures all reads have executed
			readQueue.acquire(MAX_CONCURRENT_READS);

		} catch (InterruptedException ex) {
			Logger.getLogger(HyPeerWeb.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	private void closeReadLock()
	{
		readQueue.release();
	}

	private void closeWriteLock()
	{
		//release the permits for both Semaphores
		readQueue.release(MAX_CONCURRENT_READS);
		writeLock.release();
	}


	/**
	* Clears the HyPeerWeb. And it would appear this clear is supposed
	 * to hit the persistence layer as well. Do not call internally unless
	 * you want the database to be wiped. Instead use nodes.clear()
	*
	* @pre <i>None</i>
	* @post |nodes| = 0
	*/
	public void clear() {
	if (parentSegment == null)
	{
		// TODO add write Semaphore - Lance
		openWriteLock();

		clearHypeerweb();

		closeWriteLock();
	}
	else
	{
		parentSegment.clear();
	}
	}

	public void close() {

	}

	/**
	* Accepts a list of already existent nodes and adds them to this segment.
	*
	 * 
	*/
	public void acceptNodes(List<Node> newnodes)
	{
		// Add new nodes to this segment
		for (Node node : newnodes) {
			this.addNode(node);
	}

	// Alert each nodes connections of new location
	for (Node node : newnodes) {
		node.alertNewFolds();
		node.alertNewNeighbors(node);
	}

	System.out.println("========================");
	System.out.println("Node list accepted");
	System.out.println("========================");
	}

	/**
	* Disconnects this segment from HyPeerWeb. 
	*
	 * 	1. Migrate nodes to parent segment, or a child if root.
	*	2. Alert all observers of change
	 * 
	*/	
	public void disconnectSegment()
	{	
		HyPeerWeb replacement = null;
		System.out.println("========================");
		System.out.println("Disconnecting...");

		// Get replacement segment
		if (this.isRootSegment()) {
			for (IHyPeerWebObserver observer : this.observers) {
				if (observer instanceof HyPeerWeb) {
					replacement = (HyPeerWeb) observer;
					replacement.setParentSegment(null);
				}
			}
		
			if (replacement != null)
				this.observers.remove(replacement);
		
		} else {
			replacement = this.parentSegment;
			replacement.removeListener(this);
			this.setParentSegment(null);
		}
	
		if (replacement != null)
		{
			// Migrate nodes to replacement
			for (Node node : this.nodes) {
				ObjectDB.getSingleton().remove(node.getLocalObjectId());
				node.removeLocalObjectId();
			}
		
			replacement.acceptNodes(this.nodes);
		
			// Regester observers in replacement
			for (IHyPeerWebObserver observer : this.observers) {
				observer.segmentDisconnect(this, replacement);
			}
		}
	
		clearNodes();
	
		System.out.println("Bye-bye!");
	System.out.println("========================");
	}

	public void hibernate()
	{
		if (parentSegment == null)
		{
			openWriteLock();
			hibernateHyPeerWeb();
			closeWriteLock();
		}
		else
		{
			parentSegment.hibernate();
		}
	}

	public HyPeerWebDatabase getHyPeerWebDatabase() {
		return db;
	}

	/**
	* Saves all the nodes in the HyPeerWeb and their corresponding information
	 * in the database.
	 *
	* @pre <i>None</i>
	* @post The HyPeerWeb database only contains the nodes and corresponding
	*   information for the current HyPeerWeb.
	*/
	public void saveToDatabase() {
		db.saveNodes(nodes);
	}

	/**
	* Reloads the HyPeerWeb from the database using the default database name.
	*
	* @pre <i>None</i>
	* @post All nodes are removed from the HyPeerWeb and the nodes in the existing
	*   default database are loaded into the HyPeerWeb
	*/
	public void reload() {
	reload(null);
	}

	/**
	* Reloads the HyPeerWeb from the database using the default dbName. If the
	* dbName is null then the results are loaded from the default database.
	*
	* @pre <i>None</i>
	* @post All nodes are removed from the HyPeerWeb and the nodes in the database
	*   with the name 'dbName' are loaded into the HyPeerWeb. If the 'dbName'
	*   is null nodes are loaded from the default database.
	* @param dbName
	*/
	public void reload(java.lang.String dbName) {
		nodes.clear();
		HyPeerWebDatabase.initHyPeerWebDatabase(dbName);
		nodes = db.loadNodes();
	
		for (Node node : nodes)
		{
			// TODO update the LocalObjectId.nextId to be one greater than node.localId if 
			// node.localId >= LocalObjectId.nextId
			// assigned to Chris
		
			if (LocalObjectId.getNextId() <= node.getLocalObjectId().getId())
				LocalObjectId.setNextId(node.getLocalObjectId().getId() + 1);
		
			ObjectDB.getSingleton().store(node.getLocalObjectId(), node);
		}
	}

	public void addToHyPeerWeb(Node newNode, int startNodeID) {
		if (parentSegment == null) {
			openWriteLock();
			
			Node startNode = getWebNodeInternal(startNodeID);
		
			if (startNode != null) {
		    		startNode.addToHyPeerWeb(newNode);
	       	 }

			alertNodeAdded(newNode);
		
			closeWriteLock();
		} else {
			parentSegment.addToHyPeerWeb(newNode, startNodeID);
		}
	}

	/**
	 * Adds a new node to they HyPeerWeb.
	 *
	 * Parameters:
	 *		newNode - the node to be added
	 *		startNode - a node in the HyPeerWeb we ask to do the work.
	 *
	 * @pre NewNode ≠ null AND startNode ≠ null AND newNode ≠ NULL_NODE
	 *      AND startNode ∈ nodes AND newNode ≠ nodes
	 *
	 * @post ∀ node ∈ nodes' (node ∈ nodes and has the same webId)
	 *	 AND newNode ∈ nodes AND newNode.webId = |nodes'|
	 *
	 * @param newNode
	 * @param startNode
	 */
	public void addToHyPeerWeb(Node newNode, Node startNode) {
		if (parentSegment == null) {
			openWriteLock();
		
			if (startNode != null) {
		    		startNode.addToHyPeerWeb(newNode);
	       	}

			alertNodeAdded(newNode);
		
			closeWriteLock();
		} else {
			parentSegment.addToHyPeerWeb(newNode, startNode);
		}
	}

	public void addToHyPeerWeb(int startNode) {
		System.out.println("==================");
		System.out.println("insert node id " + startNode);
		System.out.println("==================");

		Node newNode = new Node(0);
		addNode(newNode);
		addToHyPeerWeb(newNode, startNode);
	}
	
	public void addToHyPeerWeb(Node startNode) {
		System.out.println("==================");
		if (startNode != null)
		{
			System.out.println(startNode.getClass().getName());
		}
		else
		{
			System.out.println("null start node");
		}
		System.out.println("==================");

		Node newNode = new Node(0);
		addNode(newNode);
		addToHyPeerWeb(newNode, startNode);
	}

	/**
	* Adds a node to the HyPeerWeb
	*
	* Parameters:
	*   node - the node to be added to the HyPeerWeb.
	* @pre node ≠ null
	* @post node ∈ nodes
	*/
	public void addNode(Node node) {
	node.setLocalObjectId(new LocalObjectId());
	ObjectDB.getSingleton().store(node.getLocalObjectId(), node);
	nodes.add(node);
	}

	/**
	* Determines whether the indicated node is in the HyPeerWeb.
	*
	* Parameters:
	*   node - the node we are asking about.
	* @pre node ≠ null
	* @post result = node ∈ nodes
	* @param node for which to search
	* @return True/False
	*/
	public boolean contains(Node node) {
	return nodes.contains(node);
	}

	/**
	* Gets the node with the indicated index i.
	*
	* Parameters:
	*   i - the id of the node to be retrieved.
	* @pre i ≥ -1
	* @post i < size() ⇒ result = nodes[i] AND i ≥ size() ⇒ result = null;
	* @param i
	* @return Node object
	*/
	public Node getLocalNode(int i) {
		if(i < nodes.size()) {
		    return nodes.get(i);
		} else {
		    return null;
		}
	}
	
	private Node getWebNodeInternal(int webID) {
		Node startNode = findAnyNode();
	
		if (startNode == null)
		{
			closeReadLock();
			return null;
		}
	
		Node retrievedNode = startNode.retrieveNode(new WebId(webID));
		
		return retrievedNode;
	}

	public Node getWebNode(int webID) {
		if (parentSegment == null)
		{
			openReadLock();
			
			Node retrievedNode = getWebNodeInternal(webID);
	
			closeReadLock();
	
			return retrievedNode;
		} else {
			return parentSegment.getWebNode(webID);
		}
	}

	public void executeVisitor(Node startNode, Visitor visitor, Parameters parameters)
	{
		if (parentSegment == null)
		{
			openWriteLock();			
			startNode.accept(visitor, parameters);
			closeWriteLock();
		}
		else
		{
			parentSegment.executeVisitor(startNode, visitor, parameters);
		}
	}

	/**
	* Removes a node from the HyPeerWeb
	*   
	* @pre deadNode ≠ null
	* @post deadNode ∉ nodes
	*
	* @param deadNode to remove from the HyPeerWeb
	*/
	public void removeFromHyPeerWeb(Node deadNode)
	{
		if (parentSegment == null)
		{	
			openWriteLock();
			
			deadNode.removeFromHyPeerWeb();
			nodeRemovedFromHyPeerWeb(this, deadNode);
			
			deadNode.setWebId(WebId.NULL_WEB_ID);
			
			closeWriteLock();
		}
		else
		{
			parentSegment.removeFromHyPeerWeb(deadNode);
		}
	} 
	
	public void removeFromHyPeerWeb(int deadNodeID)
	{
		if (parentSegment == null)
		{	
			openWriteLock();
			
			Node deadNode = getWebNodeInternal(deadNodeID);
			
			if (deadNode != null)
			{	
				deadNode.removeFromHyPeerWeb();
				nodeRemovedFromHyPeerWeb(this, deadNode);
				
				deadNode.setWebId(WebId.NULL_WEB_ID);
			}
			else
			{
				System.out.println("=========================");
				System.out.println("no node to delete at " + deadNodeID);
			}
			
			closeWriteLock();
		}
		else
		{
			parentSegment.removeFromHyPeerWeb(deadNodeID);
		}
	} 

	/**
	* Returns the number of nodes in the HyPeerWeb.
	*
	* @pre <i>None</i>
	* @post result = |nodes|
	* @return size int
	*/

	public int getSegmentSize() {
	return nodes.size();
	}

	public int calculateWebSize() {
	if (parentSegment == null)
	{
		openReadLock();

		Node firstNode = findAnyNode();
	
		if (firstNode == null)
		{
			closeReadLock();
			return 0;
		}
		
		Node lastNode = firstNode.getState().findDeletionPoint(firstNode);
		int webId = lastNode.getWebIdValue() + 1;

		closeReadLock();

		return webId;
	}
	else
	{
		return parentSegment.calculateWebSize();
	}
	}

	public LocalObjectId getLocalObjectId() {
		return new LocalObjectId(HyPeerWeb.HYPEER_WEB_LOCAL_ID);
	}

	public Object writeReplace() throws ObjectStreamException
	{
		GlobalObjectId globalId = new GlobalObjectId(getLocalObjectId());
		return new HyPeerWebProxy(globalId);
	}

	public void setParentSegment(HyPeerWeb newParentSegment)
	{
		parentSegment = newParentSegment;
	
		if (parentSegment != null)
		{
			parentSegment.addListener(this);
		}
	
	}

	private void alertNodeAdded(Node newNode)
	{
        
		for (IHyPeerWebObserver alert : observers)
		{
            System.out.println("alert = "+alert);
            alert.nodeAddedToHyPeerWeb(this, newNode);
		}
	}

	private void alertNodeRemoved(Node deadNode)
	{
		for (IHyPeerWebObserver alert : observers)
		{
			alert.nodeRemovedFromHyPeerWeb(this, deadNode);
		}
	}

	private void alertSegmentDisconnect(HyPeerWeb replacementSegment)
	{
		for (IHyPeerWebObserver alert : observers)
		{
			alert.segmentDisconnect(this, replacementSegment);
		}
	}

	public void addListener(IHyPeerWebObserver newObserver)
	{
		observers.add(newObserver);
		System.out.println("========================");
		System.out.println("Observer added:" + newObserver.toString());
		System.out.println("========================");
	}

	public void removeListener(IHyPeerWebObserver observer)
	{
		this.observers.remove(observer);
		System.out.println("========================");
		System.out.println("Observer removed:" + observer.toString());
		System.out.println("========================");
	}

	@Override
	public Node findAnyNode()
	{
		if (nodes.size() > 0)
			return nodes.get(0);

		for (IHyPeerWebObserver observer : observers)
		{
			Node target = observer.findAnyNode();
		
			if (target != null)
				return target;
		}
	
		return null;
	}

	@Override
	public void nodeAddedToHyPeerWeb(HyPeerWeb web, Node newNode) {
		alertNodeAdded(newNode);
	}

	@Override
	public void nodeRemovedFromHyPeerWeb(HyPeerWeb web, Node deadNode) {
		alertNodeRemoved(deadNode);

		if (deadNode.isLocalNode())
		{
			nodes.remove(deadNode);
		}
	}

	@Override
	public void clearHypeerweb()
	{	
	for (Node node : nodes)
		{
		ObjectDB.getSingleton().remove(node.getLocalObjectId());
		}

		clearNodes();

		for (IHyPeerWebObserver alert : observers)
		{
			alert.clearHypeerweb();
		}
	}

	@Override
	public void hibernateHyPeerWeb()
	{
		System.out.println("========================");
		System.out.println("Good night");
		System.out.println("========================");
	
		saveToDatabase();

		for (IHyPeerWebObserver alert : observers)
		{
			alert.hibernateHyPeerWeb();
		}
	
		PeerCommunicator.stopThisConnection();
	}

	@Override
	public void segmentDisconnect(HyPeerWeb dyingSegment, HyPeerWeb replacementSegment) {
		setParentSegment(replacementSegment);
	}

	private boolean isRootSegment() {
		return this.parentSegment == null;
	}
}

